BindableObject property access micro-optimizations#33584
Conversation
| return context == null ? property.DefaultValue : context.Values.GetValue(); | ||
| } | ||
|
|
||
| internal LocalValueEnumerator GetLocalValueEnumerator() => new LocalValueEnumerator(this); |
There was a problem hiding this comment.
Removed unused methods/classes
b1d7bb1 to
39fba5c
Compare
|
Here's the SIMD implementation I used for the test. |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Removed unnecessary else block that sets default values.
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
kubaflo
left a comment
There was a problem hiding this comment.
Multimodal Review — BindableObject property access micro-optimizations
Overall Assessment: ✅ Looks good
Solid micro-optimization of a hot path. The benchmarks are convincing — Dictionary<int> + CollectionsMarshal wins clearly at ≥8 properties, and is competitive at lower counts. The approach is conservative and correct.
What works well
-
Dictionary<int, ...>keying — Switching fromBindableProperty(reference-type key → virtualGetHashCode/Equals) tointkeys eliminates that overhead entirely. Integer hashing in Dictionary is trivially cheap. -
CollectionsMarshal.GetValueRefOrAddDefaultinGetOrCreateContext— This is the highest-value change. The oldGetContext() ?? CreateAndAddContext()did two dictionary lookups on every first access. The new code does a single probe. Correctly guarded with#if NETSTANDARDfornetstandard2.0/netstandard2.1TFMs whereCollectionsMarshalis unavailable. -
ref var resultinGetValues<T>— Avoids repeated array indexing into the value-tuple array. Nice touch, and the removal of the now-redundantelsebranch (default values are already zero-initialized) was correctly identified by @MartyIX. -
LocalValueEnumerator/LocalValueEntryremoval — Confirmed zero usages across the repo. Dead code cleanup ✅ -
Interlocked.Incrementfor ID assignment — Thread-safe. And as @albyrock87 explained,BindablePropertyinstances are almost exclusively created as static fields during type initialization, so overflow is not a practical concern.
Suggestions (non-blocking)
1. Missing trailing newline in BindableObjectBenchmarker.cs
The new benchmark file doesn't end with a newline character. Most .NET tooling and dotnet format expect one. Minor but will show up as a diff artifact.
2. Consider [Benchmark(Baseline = true)]
Since the benchmark is meant to compare against the old implementation, marking one scenario as baseline would make future regression detection easier. Not required for this PR though.
3. Minor: _nextInternalId starting at int.MinValue
Functionally correct (keys just need uniqueness), but starting at 0 would be slightly more readable in debugger inspection. Non-blocking — the current approach maximizes the available ID space, which is a reasonable choice.
Correctness verification
- ✅ All
_propertiesusages checked —.Valuesiteration at lines 376 and 636 still works sinceBindablePropertyContext.Propertyholds theBindablePropertyreference. - ✅ No public API surface changes —
InternalIdisinternal, removed types wereinternal. - ✅
#if NETSTANDARDguard is correct —Controls.Core.csprojtargetsnetstandard2.0;netstandard2.1alongside platform TFMs. - ✅ No
PublicAPI.Unshipped.txtchanges needed.
🚦 Gate — Test Before and After Fix
🚦 Gate Session —
|
|
@copilot write tests for this PR |
🤖 AI Summary
📊 Review Session —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33584 | Use int InternalId keys + CollectionsMarshal.GetValueRefOrAddDefault + remove LocalValueEnumerator |
⏳ PENDING (Gate skipped — no tests) | BindableObject.cs, BindableProperty.cs |
Original PR; backed by BenchmarkDotNet data showing 10–25% improvement |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Override GetHashCode()/Equals() on BindableProperty with cached sequential int; keep BindableProperty as dict key |
✅ PASS (96/96) | BindableProperty.cs, BindableObject.cs |
Requires public API changes (GetHashCode/Equals overrides on sealed class); no key type change |
| 2 | try-fix (claude-sonnet-4.6) | ReferenceEqualityComparer.Instance as dictionary comparer + CollectionsMarshal.GetValueRefOrAddDefault |
✅ PASS (96/96) | BindableObject.cs only |
Simplest approach; no BindableProperty.cs changes; no new API entries needed |
| 3 | try-fix (gpt-5.3-codex) | Replace dictionary with List<BindablePropertyContext> + linear ReferenceEquals scan |
✅ PASS (96/96) | BindableObject.cs only |
O(n) for large property sets; correct for typical few-properties cases; more complex refactor |
| 4 | try-fix (gpt-5.4) | 2-entry per-instance MRU cache as fast-path before dictionary lookup + CollectionsMarshal fallback |
✅ PASS (96/96) | BindableObject.cs only |
Novel; adds per-instance overhead (~2 fields); benefit depends on access locality |
| PR | PR #33584 | int InternalId keys + CollectionsMarshal.GetValueRefOrAddDefault + remove LocalValueEnumerator |
BindableObject.cs, BindableProperty.cs |
Backed by BenchmarkDotNet data; most comprehensive optimization |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | No | Simple solution space fully covered by 4 attempts + PR |
| claude-sonnet-4.6 | 2 | Yes | Sorted array + binary search using InternalId — still requires InternalId, complex insertion |
| gpt-5.3-codex | 2 | Yes | Hybrid list+dictionary with threshold promotion — variant of Attempt 3, more complex |
| gpt-5.4 | 2 | Yes | Custom IEqualityComparer<BindableProperty> using InternalId hash — hybrid of Attempts 1+2 |
Exhausted: Yes — round 2 shows only complex/speculative variants remain; claude-opus confirms no new simple ideas
Selected Fix: PR's fix — The only approach backed by actual BenchmarkDotNet data showing 10–25% throughput improvement. Attempt 2 (ReferenceEqualityComparer.Instance + CollectionsMarshal) is the most compelling simpler alternative — only modifies BindableObject.cs, no BindableProperty changes, no new API surface. However, it lacks benchmark evidence. PR's int-key approach is the most impactful per benchmarks.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Performance optimization PR; no linked issue; community contribution |
| Gate | No tests detected in PR | |
| Try-Fix | ✅ COMPLETE | 4 attempts, all passing; 2 cross-pollination rounds; PR's fix selected as best |
| Report | ✅ COMPLETE |
Summary
PR #33584 is a well-benchmarked community contribution that optimizes BindableObject property storage by switching from Dictionary<BindableProperty, BindablePropertyContext> to Dictionary<int, BindablePropertyContext> (using per-property integer IDs) and applying CollectionsMarshal.GetValueRefOrAddDefault to eliminate the double-lookup in GetOrCreateContext. Benchmarks show 10–25% throughput improvement. The approach is sound, but the PR has one correctness concern and needs unit tests before merge.
Root Cause
BindableObject uses Dictionary<BindableProperty, BindablePropertyContext> for per-instance property storage. Every GetValue/SetValue call triggers virtual dispatch on BindableProperty.GetHashCode() (delegating to RuntimeHelpers.GetHashCode()). Additionally, GetOrCreateContext performed a double-lookup: first TryGetValue (check existence), then Add (insert). Both hot paths are called on every property read/write in a MAUI application.
Fix Quality
Strengths:
- Backed by real
BenchmarkDotNetdata showing 10–25% throughput improvement across 1–50 properties set - Integer key dictionary:
int.GetHashCode()is a single identity operation — no virtual dispatch, noRuntimeHelperscall CollectionsMarshal.GetValueRefOrAddDefaulteliminates double-lookup inGetOrCreateContext- Removal of
LocalValueEnumerator/LocalValueEntryis correct — confirmed unused in codebase InternalIdusesInterlocked.Increment(thread-safe), assigned once per static field init; overflow is effectively impossible#if NETSTANDARDfallback for platforms withoutCollectionsMarshalGetValues<T>ref varoptimization removes repeated array bounds checks and eliminates redundantelsebranch
Concerns requiring changes:
-
No unit tests (blocker): Gate skipped — no tests added. PR modifies
GetValue/SetValue/GetOrCreateContext(critical hot paths). Unit tests verifying correctness of the newint-keyed lookup andGetValues<T>behavior should be added toBindableObjectUnitTests.csinsrc/Controls/tests/Core.UnitTests/. -
CollectionsMarshal.GetValueRefOrAddDefaultref safety (concern): Theref contextreturned byGetValueRefOrAddDefaultis only valid until the next dictionary mutation. Ifproperty.DefaultValueCreator(this)triggers reentrantGetOrCreateContext(a property whose default value reads another property on the same object), the dictionary may resize and theref contextbecomes invalid. A comment documenting this assumption (no reentrant access) should be added. -
Counter starting at
int.MinValue(minor): Starting fromint.MinValuemeans the first property gets ID-2147483647. Starting from0(i.e., firstIncrementreturns1) is more conventional and readable. -
Benchmark file missing trailing newline (trivial):
BindableObjectBenchmarker.cslacks a final newline.
Try-Fix comparison:
All 4 try-fix alternatives pass tests. Attempt 2 (ReferenceEqualityComparer.Instance + CollectionsMarshal) is the most compelling simpler alternative — achieves both optimizations with only BindableObject.cs changes and zero changes to BindableProperty.cs. However, it has no benchmark data to compare against the PR's approach. The PR's int-key approach is the most impactful per benchmarks and the approach the author has validated end-to-end.
Selected Fix: PR's fix — backed by benchmark data; s/agent-fix-pr-picked
|
/rebase |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
### Description of Change I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage. As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that. Array + SIMD also (obviously) allocates less memory. Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging: - integer keys - CollectionMarshal on GetOrAdd ### Benchmarks | Strategy | PropertiesToSet | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------- |---------------- |------------:|---------:|---------:|-------:|-------:|----------:| | Dictionary<BindableProperty> | 1 | 65.22 ns | 0.246 ns | 0.218 ns | 0.0889 | 0.0001 | 744 B | | Array + SIMD | 1 | **52.93 ns** | 0.750 ns | 0.665 ns | 0.0678 | - | 568 B | | Dictionary<int,> | 1 | 59.29 ns | 0.563 ns | 0.440 ns | 0.0889 | 0.0001 | 744 B | | | | Dictionary<BindableProperty> | 3 | 150.70 ns | 0.344 ns | 0.322 ns | 0.1500 | 0.0005 | 1256 B | | Array + SIMD | 3 | **122.09 ns** | 0.163 ns | 0.127 ns | 0.1290 | 0.0002 | 1080 B | | Dictionary<int,> | 3 | 133.50 ns | 0.231 ns | 0.180 ns | 0.1500 | 0.0005 | 1256 B | | | | Dictionary<BindableProperty> | 8 | 426.76 ns | 0.686 ns | 0.641 ns | 0.3662 | 0.0033 | 3064 B | | Array + SIMD | 8 | **318.26 ns** | 0.438 ns | 0.409 ns | 0.3028 | 0.0024 | 2536 B | | Dictionary<int,> | 8 | 338.98 ns | 2.381 ns | 1.988 ns | 0.3662 | 0.0038 | 3064 B | | | | Dictionary<BindableProperty> | 15 | 773.49 ns | 2.593 ns | 2.298 ns | 0.5798 | 0.0095 | 4856 B | | Array + SIMD | 15 | 665.18 ns | 0.846 ns | 0.791 ns | 0.5531 | 0.0076 | 4632 B | | Dictionary<int,> | 15 | **581.04 ns** | 0.431 ns | 0.360 ns | 0.5798 | 0.0095 | 4856 B | | | | Dictionary<BindableProperty> | 30 | 1,542.20 ns | 3.342 ns | 3.126 ns | 1.1692 | 0.0381 | 9784 B | | Array + SIMD | 30 | 1,419.83 ns | 1.301 ns | 1.154 ns | 1.0796 | 0.0324 | 9032 B | | Dictionary<int,> | 30 | **1,165.86 ns** | 1.599 ns | 1.496 ns | 1.1692 | 0.0381 | 9784 B | | | | Dictionary<BindableProperty> | 50 | 2,648.69 ns | 3.759 ns | 2.935 ns | 2.0828 | 0.1183 | 17448 B | | Array + SIMD | 50 | 2,587.54 ns | 3.111 ns | 2.429 ns | 1.8196 | 0.0916 | 15224 B | | Dictionary<int,> | 50 | **1,997.17 ns** | 2.174 ns | 2.033 ns | 2.0828 | 0.1183 | 17448 B |
Description of Change
I tried to improve the access time on BindableProperty given they're being accessed a lot during the application usage.
As we can see using Array + SIMD is actually performing better when the number of BindableProperty set on a BindableObject is less than ~10, though it becomes worse after that.
Array + SIMD also (obviously) allocates less memory.
Considering that SIMD may perform differently on different platforms I felt it would be better to simply improve the current Dictionary based implementation by leveraging:
Benchmarks